Descriptor Example: Attribute Validation

LineItem Take #3: A Simple Descriptor


In [5]:
class Quantity:
    
    def __init__(self, storage_name):
        self.storage_name = storage_name
        
    def __set__(self, instance, value):
        if value > 0:
            instance.__dict__[self.storage_name] = value
        else:
            raise ValueError('value must be > 0')
            
class LineItem:
    weight = Quantity('weight')
    price = Quantity('price')
    
    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price
        
    def subtotal(self):
        return self.weight * self.price

In [9]:
truffle = LineItem('White truffle', 100, 0)


---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-9-9db5146ae683> in <module>()
----> 1 truffle = LineItem('White truffle', 100, 0)

<ipython-input-5-d176b7abc5af> in __init__(self, description, weight, price)
     17         self.description = description
     18         self.weight = weight
---> 19         self.price = price
     20 
     21     def subtotal(self):

<ipython-input-5-d176b7abc5af> in __set__(self, instance, value)
      8             instance.__dict__[self.storage_name] = value
      9         else:
---> 10             raise ValueError('value must be > 0')
     11 
     12 class LineItem:

ValueError: value must be > 0

In [10]:
truffle = LineItem('White truffle', 100, 1)

In [13]:
truffle.__dict__


Out[13]:
{'description': 'White truffle', 'price': 1, 'weight': 100}

LineItem Take #4: Automatic Storage Attribute Names


In [14]:
class Quantity:
    __counter = 0
    
    def __init__(self):
        cls = self.__class__
        prefix = cls.__name__
        index = cls.__counter
        self.storage_name = '_{}#{}'.format(prefix, index)
        cls.__counter += 1
        
    def __get__(self, instance, owner):
        return getattr(instance, self.storage_name)
    
    def __set__(self, instance, value):
        if value > 0:
            setattr(instance, self.storage_name, value)
        else:
            raise ValueError('value must be > 0')
            
class LineItem:
    weight = Quantity()
    price = Quantity()
    
    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price
        
    def subtotal(self):
        return self.weight * self.price

In [15]:
coconuts = LineItem('Brazilian coconut', 20, 17.95)

In [16]:
coconuts.weight, coconuts.price


Out[16]:
(20, 17.95)

In [17]:
getattr(coconuts, '_Quantity#0'), getattr(coconuts, '_Quantity#1')


Out[17]:
(20, 17.95)

In [18]:
LineItem.weight


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-18-b9c12b383de2> in <module>()
----> 1 LineItem.weight

<ipython-input-14-f1a254c1447e> in __get__(self, instance, owner)
     10 
     11     def __get__(self, instance, owner):
---> 12         return getattr(instance, self.storage_name)
     13 
     14     def __set__(self, instance, value):

AttributeError: 'NoneType' object has no attribute '_Quantity#0'

In [19]:
class Quantity:
    __counter = 0
    
    def __init__(self):
        cls = self.__class__
        prefix = cls.__name__
        index = cls.__counter
        self.storage_name = '_{}#{}'.format(prefix, index)
        cls.__counter += 1
        
    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return getattr(instance, self.storage_name)
    
    def __set__(self, instance, value):
        if value > 0:
            setattr(instance, self.storage_name, value)
        else:
            raise ValueError('value must be > 0')
            
class LineItem:
    weight = Quantity()
    price = Quantity()
    
    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price
        
    def subtotal(self):
        return self.weight * self.price

In [20]:
LineItem.price


Out[20]:
<__main__.Quantity at 0xa063d3ac88>

In [21]:
br_nuts = LineItem('Brazil nuts', 10, 34.95)
br_nuts.price


Out[21]:
34.95

LineItem Take #5: A New Descriptor Type


In [1]:
import abc

class AutoStorage:
    __counter = 0
    
    def __init__(self):
        cls = self.__class__
        prefix = cls.__name__
        index = cls.__counter
        self.storage_name = '_{}#{}'.format(prefix, index)
        cls.__counter += 1
        
    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return getattr(instance, self.storage_name)
    
    def __set__(self, instance, value):
        setattr(instance, self.storage_name, value)
        
class Validated(abc.ABC, AutoStorage):
    
    def __set__(self, instance, value):
        value = self.validate(instance, value)
        super().__set__(instance, value)
        
    @abc.abstractmethod
    def validate(self, instance, value):
        """return validated value or raise ValueError"""
        
class Quantity(Validated):
    """a number greater than zero"""
    
    def validate(self, instance, value):
        if value <= 0:
            raise ValueError('value must be > 0')
        return value
    
class NonBlank(Validated):
    """a string with at least one non-space character"""
    
    def validate(self, instance, value):
        value = value.strip()
        if len(value) == 0:
            raise ValueError('value cannot be empty or blank')
        return value

In [3]:
class LineItem:
    description = NonBlank()
    weight = Quantity()
    price = Quantity()
    
    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price
        
    def subtotal(self):
        return self.weight * self.price

Overriding Versus Nonoverriding Descriptors


In [27]:
### auxiliary functions for display only ###

def cls_name(obj_or_cls):
    cls = type(obj_or_cls)
    if cls is type:
        cls = obj_or_cls
    return cls.__name__.split('.')[-1]

def display(obj):
    cls = type(obj)
    if cls is type:
        return '<class {}>'.format(obj.__name__)
    elif cls in [type(None), int]:
        return repr(obj)
    else:
        return '<{} object>'.format(cls_name(obj))
    
def print_args(name, *args):
    pseudo_args = ', '.join(display(x) for x in args)
    print('-> {}.__{}__({})'.format(cls_name(args[0]), name, pseudo_args))
    
### essential classes for this example ###

class Overriding:
    """a.k.a. data descriptor or enforded descriptor"""
    
    def __get__(self, instance, owner):
        print_args('get', self, instance, owner)
        
    def __set__(self, instance, value):
        print_args('set', self, instance, value)
        
class OverridingNoGet:
    """an overriding descriptor without ``__get__``"""
    
    def __set__(self, instance, value):
        print_args('set', self, instance, value)

class NonOverriding:
    """a.k.a. non-data or shadowable descriptor"""
    
    def __get__(self, instance, owner):
        print_args('get', self, instance, owner)

class Managed:
    over = Overriding()
    over_no_get = OverridingNoGet()
    non_over = NonOverriding()
    
    def spam(self):
        print('-> Managed.spam({})'.format(display(self)))

Overriding Descriptor


In [29]:
obj = Managed()
obj.over


-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)

In [30]:
Managed.over


-> Overriding.__get__(<Overriding object>, None, <class Managed>)

In [31]:
obj.over = 7


-> Overriding.__set__(<Overriding object>, <Managed object>, 7)

In [32]:
obj.over


-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)

In [33]:
obj.__dict__['over'] = 8

In [34]:
vars(obj)


Out[34]:
{'over': 8}

In [35]:
obj.over


-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)

Overriding Descriptor Without get


In [36]:
obj.over_no_get


Out[36]:
<__main__.OverridingNoGet at 0xa063ca3048>

In [37]:
Managed.over_no_get


Out[37]:
<__main__.OverridingNoGet at 0xa063ca3048>

In [38]:
obj.over_no_get = 7


-> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 7)

In [39]:
obj.over_no_get


Out[39]:
<__main__.OverridingNoGet at 0xa063ca3048>

In [40]:
obj.__dict__['over_no_get'] = 9
obj.over_no_get


Out[40]:
9

In [41]:
obj.over_no_get = 7
obj.over_no_get


-> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 7)
Out[41]:
9

Nonoverriding Descriptor


In [42]:
obj = Managed()
obj.non_over


-> NonOverriding.__get__(<NonOverriding object>, <Managed object>, <class Managed>)

In [43]:
obj.non_over = 7
obj.non_over


Out[43]:
7

In [44]:
Managed.non_over


-> NonOverriding.__get__(<NonOverriding object>, None, <class Managed>)

In [45]:
del obj.non_over

In [46]:
obj.non_over


-> NonOverriding.__get__(<NonOverriding object>, <Managed object>, <class Managed>)

Overwriting a Descriptor in the Class


In [47]:
obj = Managed()
Managed.over = 1
Managed.over_no_get = 2
Managed.non_over = 3
obj.over, obj.over_no_get, obj.non_over


Out[47]:
(1, 2, 3)

Methods Are Descriptors


In [48]:
obj = Managed()
obj.spam


Out[48]:
<bound method Managed.spam of <__main__.Managed object at 0x000000A063D6A0F0>>

In [49]:
Managed.spam


Out[49]:
<function __main__.Managed.spam>

In [50]:
obj.spam = 7
obj.spam


Out[50]:
7

In [51]:
import collections

class Text(collections.UserString):
    
    def __repr__(self):
        return 'Text({!r})'.format(self.data)
    
    def reverse(self):
        return self[::-1]

In [52]:
word = Text('forward')
word


Out[52]:
Text('forward')

In [53]:
word.reverse()


Out[53]:
Text('drawrof')

In [54]:
Text.reverse(Text('backward'))


Out[54]:
Text('drawkcab')

In [55]:
type(Text.reverse), type(word.reverse)


Out[55]:
(function, method)

In [56]:
list(map(Text.reverse, ['repaid', (10, 20, 30), Text('stressed')]))


Out[56]:
['diaper', (30, 20, 10), Text('desserts')]

In [57]:
Text.reverse.__get__(word)


Out[57]:
<bound method Text.reverse of Text('forward')>

In [58]:
Text.reverse.__get__(None, Text)


Out[58]:
<function __main__.Text.reverse>

In [59]:
word.reverse


Out[59]:
<bound method Text.reverse of Text('forward')>

In [60]:
word.reverse.__self__


Out[60]:
Text('forward')

In [61]:
word.reverse.__func__ is Text.reverse


Out[61]:
True

Descriptor docstring and Overriding Deletion


In [4]:
help(LineItem.weight)


Help on Quantity in module __main__ object:

class Quantity(Validated)
 |  a number greater than zero
 |  
 |  Method resolution order:
 |      Quantity
 |      Validated
 |      abc.ABC
 |      AutoStorage
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  validate(self, instance, value)
 |      return validated value or raise ValueError
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  
 |  __abstractmethods__ = frozenset()
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from Validated:
 |  
 |  __set__(self, instance, value)
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from abc.ABC:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from AutoStorage:
 |  
 |  __get__(self, instance, owner)
 |  
 |  __init__(self)
 |      Initialize self.  See help(type(self)) for accurate signature.


In [5]:
help(LineItem)


Help on class LineItem in module __main__:

class LineItem(builtins.object)
 |  Methods defined here:
 |  
 |  __init__(self, description, weight, price)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  subtotal(self)
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)
 |  
 |  description
 |      a string with at least one non-space character
 |  
 |  price
 |      a number greater than zero
 |  
 |  weight
 |      a number greater than zero


In [ ]: